﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Windows.Forms;
using System.Threading;

namespace DirectorySynchronizer
{
    public sealed class DirSynchronizer : ICancelable, IDisposable
    {
        private bool cancelsync = false;
        private bool previewonly = false;
        private bool donotrecurse = false;
        private bool taginsteadofoverwrite = false;
        private bool taginsteadofdelete = false;
        private bool ignoreoverwriteanddelete = false;
        private bool copyattributes = false;
        private bool tagcopynotoldfile = false;
        private bool differingfilesystems = true;
        private long dircount = 0;
        private long dirsdone = 0;
        private long filecount = 0;
        private DateTime syncstartdatetime;
        private string basesourcepath = null;
        private string basetargetpath = null;
        private string tagstring = string.Empty;
        private readonly TimeSpan onehour = new TimeSpan(0, 1, 0, 0, 0);
        private IProgressDisplay ProgressDisplay = null;
        public DirSyncDS.SyncItemsDataTable syncresultsDS = null;


        /// <summary>
        /// Constructor for a DirectorySynchronizer object
        /// </summary>
        /// <param name="argsourcepath">Directory path to original directory.</param>
        /// <param name="argtargetpath">Directory path to directory to synchronize to original directory.</param>
        /// <param name="argPreviewOnly">Pass true to get a report of file operations neccessary for synchronization without actually copying/deleting/overwriting any files.</param>
        /// <param name="argProgressDisplay">Object capable of displaying progress, null throws error.</param>
        public DirSynchronizer(DirSyncOptions argDirSyncOptions, IProgressDisplay argProgressDisplay)
        {
            if (argProgressDisplay == null) { throw new ApplicationException("Unexpected null object reference in constructor."); }
            previewonly = argDirSyncOptions.PreviewOnly;
            donotrecurse = argDirSyncOptions.DoNotRecurse;
            basesourcepath = argDirSyncOptions.SourceDirectory;
            basetargetpath = argDirSyncOptions.TargetDirectory;
            tagstring = argDirSyncOptions.TagString;
            taginsteadofoverwrite = argDirSyncOptions.TagInsteadOfOverwrite;
            taginsteadofdelete = argDirSyncOptions.TagInsteadOfDelete;
            tagcopynotoldfile = argDirSyncOptions.TagCopyNotOldFile;
            ignoreoverwriteanddelete = argDirSyncOptions.IgnoreOverwriteAndDelete;
            copyattributes = argDirSyncOptions.CopyAttributes;
            ProgressDisplay = argProgressDisplay;
            syncresultsDS = new DirSyncDS.SyncItemsDataTable();
        }

        private void CountDirsAndFiles(string workpath, ref long argdircount, ref long argfilecount, bool argupdateprogress)
        {
            if (cancelsync) { return; }
            string[] workfiles = Directory.GetFiles(workpath);
            argfilecount += workfiles.Length;
            if (!donotrecurse)
            {
                string[] workdirs = Directory.GetDirectories(workpath);
                argdircount += workdirs.Length;
                foreach (string workdir in workdirs)
                {
                    CountDirsAndFiles(workdir, ref argdircount, ref argfilecount, argupdateprogress);
                }
            }
            if (argupdateprogress && ProgressDisplay != null)
            {
                string updatestring = "Scanning synchronization source extent.  Found " + dircount.ToString() + " directories and " + filecount.ToString() + " files in synchronization source.";
                ProgressDisplay.UpdateProgress(updatestring, 0, filecount);
            }
        }

        /// <summary>
        /// Start synchronization thread process.
        /// </summary>
        public void StartDirectorySynchronization()
        {
            if (basesourcepath.Length > 2 && basetargetpath.Length > 2)
            {
                if (basesourcepath.Substring(1, 2) == ":\\" && basetargetpath.Substring(1, 2) == ":\\")
                {
                    DriveInfo sourcedrive = new DriveInfo(basesourcepath[0].ToString());
                    DriveInfo targetdrive = new DriveInfo(basetargetpath[0].ToString());
                    if (sourcedrive.DriveFormat == targetdrive.DriveFormat)
                    {
                        differingfilesystems = false;
                    }
                }
            }

            syncstartdatetime = DateTime.Now;
            CountDirsAndFiles(basesourcepath, ref filecount, ref dircount, true);
            try
            {
                SyncDirectories(basesourcepath, basetargetpath);
            }
            catch (Exception ex)
            {
                MessageBox.Show("Synchronization interrupted by error: \r\n" + ex.Message, "Directory Synchronizer: Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            TimeSpan syncduration = new TimeSpan(DateTime.Now.Ticks - syncstartdatetime.Ticks);
            if (ProgressDisplay != null && !ProgressDisplay.IsDisposed && !ProgressDisplay.Disposing)
            {
                StringBuilder sb = new StringBuilder();
                if (cancelsync) { sb.Append("Synchronization cancelled after "); }
                else { sb.Append("Synchronization complete.  Operation took "); }
                int durationmin = (int)Math.Floor(syncduration.TotalMinutes);
                sb.Append(durationmin != 1 ? durationmin.ToString() + " minutes " : " 1 minute and ");
                sb.Append(syncduration.Seconds != 1 ? syncduration.Seconds.ToString() + " seconds." : " 1 second.");
                if (cancelsync)
                {
                    sb.Append("  " + dirsdone.ToString() + " of " + dircount + " were synchronized.");
                    ProgressDisplay.UpdateProgress(sb.ToString(), dirsdone, dircount);
                }
                else
                {
                    ProgressDisplay.UpdateProgress(sb.ToString(), dircount, dircount);
                }
                ProgressDisplay.ProcessFinished();
            }
        }
        /// <summary>
        /// Notes: copy attributes does copy the Attributes and all three Created/Changed/Accessed datetimes
        ///        attributes, FileCreationDateTime, and FileLastWriteDateTime
        ///        are not compared to determine if a synchronization copy operation is required
        /// </summary>
        /// <param name="sourcepath"></param>
        /// <param name="targetpath"></param>
        private void SyncDirectories(string sourcepath, string targetpath)
        {
            if (cancelsync) { return; }
            string[] targetdirs = Directory.GetDirectories(targetpath);

            #region //delete any directories in the target that are not in the source
            if (!donotrecurse)
            {
                foreach (string targetdir in targetdirs)
                {
                    if (cancelsync) { break; }
                    string[] targetdirparts = targetdir.Split(Path.DirectorySeparatorChar);
                    if (!Directory.Exists(Path.Combine(sourcepath, targetdirparts[targetdirparts.Length - 1])))
                    {
                        long targetdircount = 0;
                        long targetdirfilecount = 0;
                        CountDirsAndFiles(targetdir, ref targetdircount, ref targetdirfilecount, false);
                        DirSyncDS.SyncItemsRow deleteddirrow = syncresultsDS.NewSyncItemsRow();
                        deleteddirrow.Timestamp = DateTime.Now;
                        deleteddirrow.Path = targetdir;
                        deleteddirrow.Filename = "<|Directory>";
                        deleteddirrow.Reason = "dir. not in source";
                        if (!previewonly)
                        {
                            if (ignoreoverwriteanddelete)
                            {
                                deleteddirrow.Action = "dir. deletion ignored";
                            }
                            else if (taginsteadofdelete)
                            {
                                int newtargetdirtag = 1;
                                string newtargetdir = targetdir + newtargetdirtag.ToString() + tagstring;
                                while (Directory.Exists(newtargetdir))
                                { newtargetdir = targetdir + (++newtargetdirtag).ToString() + tagstring; }
                                deleteddirrow.Action = "dir. moved to tag";
                                Directory.Move(targetdir, newtargetdir);
                            }
                            else
                            {
                                deleteddirrow.Action = "deleted with " + targetdircount.ToString() + " subdirectories and " + targetdirfilecount.ToString() + " files";
                                Directory.Delete(targetdir, true);
                            }
                        }
                        syncresultsDS.AddSyncItemsRow(deleteddirrow);
                    }
                }
            }
            #endregion

            #region //delete files in target that are not in source
            string[] targetfiles = Directory.GetFiles(targetpath);
            foreach (string targetfile in targetfiles)
            {
                if (cancelsync) { break; }
                string equivalentsourcefile = Path.Combine(sourcepath, Path.GetFileName(targetfile));
                if (!File.Exists(equivalentsourcefile))
                {
                    DirSyncDS.SyncItemsRow deletedfilerow = syncresultsDS.NewSyncItemsRow();
                    deletedfilerow.Timestamp = DateTime.Now;
                    deletedfilerow.Path = targetpath;
                    deletedfilerow.Filename = Path.GetFileName(targetfile);
                    deletedfilerow.Reason = "file not in source";
                    if (!previewonly)
                    {
                        if (ignoreoverwriteanddelete)
                        {
                            deletedfilerow.Action = "file deletion ignored";
                        }
                        else if (taginsteadofdelete)
                        {
                            string newfile = targetfile;
                            string newfilenamenoext = Path.GetFileNameWithoutExtension(targetfile);
                            string newfilenameext = Path.GetExtension(targetfile);
                            int filecount = 0;
                            while (File.Exists(Path.Combine(targetpath, newfile)))
                            {
                                //if (filecount == int.MaxValue) { } //ignoring this possibility
                                newfile = newfilenamenoext + (++filecount).ToString() + tagstring + newfilenameext;
                            }
                            deletedfilerow.Action = "file moved to '" + newfile + "'";
                            File.Move(targetfile, Path.Combine(targetpath, newfile));
                        }
                        else
                        {
                            deletedfilerow.Action = "file deleted";
                            File.Delete(targetfile);
                        }
                    }
                    syncresultsDS.AddSyncItemsRow(deletedfilerow);
                }
            }
            #endregion

            #region //copy files in source that are not in target or overwrite
            string[] sourcefiles = Directory.GetFiles(sourcepath);
            foreach (string sourcefile in sourcefiles)
            {
                if (cancelsync) { break; }
                string sourcefilename = Path.GetFileName(sourcefile);
                string targetfile = Path.Combine(targetpath, sourcefilename);
                if (!File.Exists(targetfile))
                {
                    DirSyncDS.SyncItemsRow copiedfilerow = syncresultsDS.NewSyncItemsRow();
                    copiedfilerow.Timestamp = DateTime.Now;
                    copiedfilerow.Path = targetpath;
                    copiedfilerow.Filename = sourcefilename;
                    copiedfilerow.Reason = "not in target dir.";
                    if (!previewonly)
                    {
                        copiedfilerow.Action = "copy added to target";
                        string updatestring = dirsdone.ToString() + " of " + dircount.ToString() + " file directories completed, copying file '" + sourcefile + "'";
                        ProgressDisplay.UpdateProgress(updatestring, dirsdone, dircount);
                        File.Copy(sourcefile, targetfile);
                        if (copyattributes)
                        {
                            FileInfo workfi = new FileInfo(sourcefile);
                            File.SetAttributes(targetfile, workfi.Attributes);
                            File.SetCreationTime(targetfile, workfi.CreationTime);
                            File.SetLastAccessTime(targetfile, workfi.LastAccessTime);
                            File.SetLastWriteTime(targetfile, workfi.LastWriteTime);
                            //File.SetAccessControl(targetfile, workfi.GetAccessControl());
                            copiedfilerow.Action = copiedfilerow.Action + ", attr. copied";
                        }
                    }
                    else { copiedfilerow.Action = "copy to target"; }
                    syncresultsDS.AddSyncItemsRow(copiedfilerow);
                }
                else
                {
                    bool copyneeded = false;
                    bool copyattributesneeded = false;
                    string copyreason = string.Empty;
                    FileInfo sourcefileinfo = new FileInfo(sourcefile);
                    FileInfo targetfileinfo = new FileInfo(targetfile);
                    if (sourcefileinfo.Length != targetfileinfo.Length)
                    {
                        copyneeded = true;
                        copyreason = "different lengths";
                    }
                    else
                    {
                        TimeSpan testtimespan = new TimeSpan(Math.Abs(sourcefileinfo.LastWriteTime.Ticks - targetfileinfo.LastWriteTime.Ticks));
                        if (differingfilesystems) //NTFS has finer timestamp accuracy than FAT32
                        {
                            if (!(testtimespan.TotalSeconds < 5 ||
                                Math.Abs(testtimespan.Add(onehour).TotalSeconds) < 5 ||
                                Math.Abs(testtimespan.Subtract(onehour).TotalSeconds) < 5))
                            {
                                copyneeded = true;
                                copyreason = "timestamp different";
                            }
                        }
                        else if (testtimespan.TotalSeconds > 1)
                        {
                            copyneeded = true;
                            copyreason = "timestamp different";
                        }
                    }
                    if (copyattributes && (sourcefileinfo.Attributes != targetfileinfo.Attributes)) { copyattributesneeded = true; }
                    if (copyneeded || copyattributesneeded)
                    {
                        DirSyncDS.SyncItemsRow copiedfilerow = syncresultsDS.NewSyncItemsRow();
                        copiedfilerow.Timestamp = DateTime.Now;
                        copiedfilerow.Path = targetpath;
                        copiedfilerow.Filename = sourcefilename;
                        copiedfilerow.Reason = copyreason;
                        copiedfilerow.Action = string.Empty;
                        if (copyneeded)
                        {
                            if (ignoreoverwriteanddelete)
                            {
                                copiedfilerow.Action = "ignored overwrite";
                            }
                            else
                            {
                                if (previewonly)
                                {
                                    copiedfilerow.Action = "overwrite file";
                                }
                                else
                                {
                                    if (taginsteadofoverwrite)
                                    {
                                        string newfile = targetfile;
                                        string newfilenamenoext = Path.GetFileNameWithoutExtension(targetfile);
                                        string newfilenameext = Path.GetExtension(targetfile);
                                        int filecount = 0;
                                        while (File.Exists(newfile))
                                        {
                                            //if (filecount == int.MaxValue) { } //ignoring this possibility
                                            newfile = newfilenamenoext + (++filecount).ToString() + tagstring + newfilenameext;
                                        }
                                        if (tagcopynotoldfile)
                                        {
                                            copiedfilerow.Action = "copied to '" + newfile + "'";
                                            targetfile = Path.Combine(targetpath, newfile);
                                        }
                                        else
                                        {
                                            copiedfilerow.Action = "copied, existing file moved to '" + newfile + "'";
                                            File.Move(targetfile, Path.Combine(targetpath, newfile));
                                        }
                                    }
                                    else
                                    {
                                        copiedfilerow.Action = "file overwritten";
                                        File.Delete(targetfile);
                                    }
                                    string updatestring = dirsdone.ToString() + " of " + dircount.ToString() + " source file directories completed, copying file '" + sourcefile + "'";
                                    ProgressDisplay.UpdateProgress(updatestring, dirsdone, dircount);
                                    File.Copy(sourcefile, targetfile);
                                }
                            }
                        }
                        if (copyattributesneeded || (copyneeded && copyattributes))
                        {
                            File.SetAttributes(targetfile, sourcefileinfo.Attributes);
                            File.SetCreationTime(targetfile, sourcefileinfo.CreationTime);
                            File.SetLastAccessTime(targetfile, sourcefileinfo.LastAccessTime);
                            File.SetLastWriteTime(targetfile, sourcefileinfo.LastWriteTime);
                            //File.SetAccessControl(targetfile, sourcefileinfo.GetAccessControl() + targetfileinfo.GetAccessControl());
                            if (copiedfilerow.Action == string.Empty)
                            { copiedfilerow.Action = "attr. copied"; }
                            else { copiedfilerow.Action = copiedfilerow.Action + ", attr. copied"; }
                        }
                        if (syncresultsDS != null) { syncresultsDS.AddSyncItemsRow(copiedfilerow); }
                    }
                }
            }
            #endregion

            #region //create directories not in target that are in source and recurse
            //for each directory in source
            if (!donotrecurse)
            {
                string[] sourcedirs = Directory.GetDirectories(sourcepath);
                foreach (string sourcedir in sourcedirs)
                {
                    if (cancelsync) { break; }
                    string[] workdirparts = sourcedir.Split(Path.DirectorySeparatorChar);
                    string workdir = workdirparts[workdirparts.Length - 1];
                    string targetdir = Path.Combine(targetpath, workdir);
                    if (!Directory.Exists(targetdir))
                    {
                        DirSyncDS.SyncItemsRow createddirrow = syncresultsDS.NewSyncItemsRow();
                        createddirrow.Timestamp = DateTime.Now;
                        createddirrow.Path = targetpath;
                        createddirrow.Filename = "<|Directory>";
                        if (previewonly) { createddirrow.Action = "create"; }
                        else { createddirrow.Action = "created"; }
                        createddirrow.Reason = "dir. not in target";
                        syncresultsDS.AddSyncItemsRow(createddirrow);
                        if (!previewonly)
                        {
                            Directory.CreateDirectory(targetdir);
                            //if (copyattributes)
                            //{
                            //    DirectoryInfo sourcedi = new DirectoryInfo(sourcedir);
                            //    Directory.SetAccessControl(targetdir, sourcedi.GetAccessControl());
                            //    targetdi.Attributes = sourcedi.Attributes;
                            //    targetdi.CreationTimeUtc = sourcedi.CreationTimeUtc;
                            //    targetdi.LastAccessTimeUtc = sourcedi.LastAccessTimeUtc;
                            //}
                        }
                    }
                    if (!previewonly) { SyncDirectories(sourcedir, targetdir); }
                }
            }
            #endregion

            //update progress display data
            if (ProgressDisplay != null)
            {
                string updatestring = dirsdone.ToString() + " of " + dircount.ToString() + " directories completed";
                ProgressDisplay.UpdateProgress(updatestring, dirsdone, dircount);
            }
            dirsdone++;
        }

        public bool Cancelled
        {
            get { return cancelsync; }
        }
        public void Cancel()
        {
            cancelsync = true;
            if (ProgressDisplay != null)
            {
                ProgressDisplay.UpdateProgress("Synchronization cancelled.", dirsdone, dircount);
            }
        }

        public void Dispose()
        {
            ProgressDisplay = null;
            if (syncresultsDS != null)
            {
                syncresultsDS.Dispose();
                syncresultsDS = null;
            }
        }
    }
}
